﻿/**
 * Peter
 * Created by: Peter Development Team
 *    http://peter.codeplex.com/
 *
 * GNU General Public License version 2 (GPLv2)
 *    http://peter.codeplex.com/license
 *
 *  This code is provided on an AS IS basis, with no WARRANTIES,
 *  CONDITIONS or GUARANTEES of any kind.
 *
 **/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using ISegment = ICSharpCode.AvalonEdit.Document.ISegment;

namespace Peter.Logic.AvalonEdit
{
   /// <summary>
   /// Service used to mark text in an editor.
   /// </summary>
   public class TextMarkerService : DocumentColorizingTransformer, IBackgroundRenderer
   {
      private readonly TextEditor m_Editor;
      private readonly TextSegmentCollection<TextMarker> m_Markers;

      /// <summary>
      /// Creates a new text marker service for the given editor.
      /// </summary>
      /// <param name="editor"></param>
      public TextMarkerService (TextEditor editor)
      {
         if (editor == null)
            throw new ArgumentNullException ("editor");
         this.m_Editor = editor;
         this.m_Markers = new TextSegmentCollection<TextMarker> ();
         this.m_Editor.TextArea.TextView.LineTransformers.Add (this);
         this.m_Editor.TextArea.TextView.BackgroundRenderers.Add (this);
      }

      /// <summary>
      /// Creates a new text marker.
      /// </summary>
      /// <param name="startOffset">Start offset in the document.</param>
      /// <param name="length">Length of marker</param>
      /// <returns>The newly created text marker.</returns>
      public TextMarker Create (int startOffset, int length)
      {
         int textLength = m_Editor.Document.TextLength;
         if (startOffset < 0 || startOffset > textLength)
            throw new ArgumentOutOfRangeException ("startOffset");
         if (length < 0 || startOffset + length > textLength)
            throw new ArgumentOutOfRangeException ("length");

         var marker = new TextMarker (this, startOffset, length);
         m_Markers.Add (marker);
         this.Redraw (marker);
         return marker;
      }

      /// <summary>
      /// Gets the attached editor.
      /// </summary>
      protected TextEditor Editor
      {
         get { return this.m_Editor; }
      }

      /// <summary>
      /// Gets the current list of markers.
      /// </summary>
      public IEnumerable<TextMarker> TextMarkers
      {
         get { return m_Markers; }
      }

      /// <summary>
      /// Get the number of text markers.
      /// </summary>
      public int TextMarkerCount
      {
         get { return this.m_Markers.Count; }
      }

      /// <summary>
      /// Gets any markers for the given offset.
      /// </summary>
      /// <param name="offset">The offset to get markers for.</param>
      /// <returns>The list of markers at the given offset.</returns>
      public IEnumerable<TextMarker> GetMarkersAtOffset (int offset)
      {
         return m_Markers.FindSegmentsContaining (offset);
      }

      /// <summary>
      /// Removes all markers.
      /// </summary>
      public void RemoveAll ()
      {
         foreach (var textMarker in this.m_Markers)
         {
            textMarker.Delete ();
         }
         this.m_Markers.Clear ();
      }

      /// <summary>
      /// Removes the given text marker.
      /// </summary>
      /// <param name="marker">Marker to remove.</param>
      public void Remove (TextMarker marker)
      {
         if (marker == null)
            throw new ArgumentNullException ("marker");
         var m = marker;
         if (m_Markers.Remove (m))
         {
            Redraw (m);
            m.OnDeleted ();
         }
      }

      /// <summary>
      /// Redraws the specified text segment.
      /// </summary>
      internal void Redraw (ISegment segment)
      {
         this.m_Editor.TextArea.TextView.Redraw (segment);
      }

      #region DocumentColorizingTransformer

      /// <summary>
      /// Override this method to colorize an individual document line.
      /// </summary>
      protected override void ColorizeLine (DocumentLine line)
      {
         if (m_Markers == null)
            return;
         int lineStart = line.Offset;
         int lineEnd = lineStart + line.Length;
         foreach (var marker in m_Markers.FindOverlappingSegments (lineStart, line.Length))
         {
            if (marker.ForegroundColor != null)
            {
               Brush foregroundBrush = new SolidColorBrush (marker.ForegroundColor.Value);
               foregroundBrush.Freeze ();
               ChangeLinePart (Math.Max (marker.StartOffset, lineStart),
                  Math.Min (marker.EndOffset, lineEnd),
                  element => element.TextRunProperties.SetForegroundBrush (foregroundBrush));
            }
         }
      }

      #endregion

      #region IBackgroundRenderer

      /// <summary>
      /// Gets the layer on which this background renderer should draw.
      /// </summary>
      public KnownLayer Layer
      {
         get
         {
            // draw behind selection
            return KnownLayer.Selection;
         }
      }

      /// <summary>
      /// Causes the background renderer to draw.
      /// </summary>
      public void Draw (TextView textView, DrawingContext drawingContext)
      {
         if (textView == null)
            throw new ArgumentNullException ("textView");
         if (drawingContext == null)
            throw new ArgumentNullException ("drawingContext");
         if (m_Markers == null || !textView.VisualLinesValid)
            return;
         var visualLines = textView.VisualLines;
         if (visualLines.Count == 0)
            return;
         int viewStart = visualLines.First ().FirstDocumentLine.Offset;
         int viewEnd = visualLines.Last ().LastDocumentLine.Offset + visualLines.Last ().LastDocumentLine.Length;
         foreach (TextMarker marker in m_Markers.FindOverlappingSegments (viewStart, viewEnd - viewStart))
         {
            if (marker.BackgroundColor != null)
            {
               var geoBuilder = new BackgroundGeometryBuilder { AlignToWholePixels = true, CornerRadius = 0 };
               geoBuilder.AddSegment (textView, marker);
               Geometry geometry = geoBuilder.CreateGeometry ();
               if (geometry != null)
               {
                  Color color = marker.BackgroundColor.Value;
                  var brush = new SolidColorBrush (color);
                  brush.Freeze ();
                  drawingContext.DrawGeometry (brush, null, geometry);
               }
            }
         }
      }
      #endregion

      /// <summary>
      /// Removes this background renderer from the stack to renderers.
      /// </summary>
      public void Remove ()
      {
         this.RemoveAll ();
         if (this.m_Editor.TextArea.TextView.LineTransformers.Contains (this))
            this.m_Editor.TextArea.TextView.LineTransformers.Remove (this);
         if (this.m_Editor.TextArea.TextView.BackgroundRenderers.Contains (this))
            this.m_Editor.TextArea.TextView.BackgroundRenderers.Remove (this);
      }
   }
}
